﻿using Sustainalytics.Entities.ESGRatings;
using Sustainalytics.ESGRatings.BusinessLogic.Computing.Exceptions;
using Sustainalytics.ESGRatings.Computing.Entities;
using Sustainalytics.ESGRatings.Computing.Entities.ESG;
using Sustainalytics.ESGRatings.Primitives;
using Sustainalytics.ESGRatings.Primitives.Exceptions;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using CompanyWeights = Sustainalytics.ESGRatings.Computing.Entities.ESG.CompanyWeights;
using ReferenceUniverse = Sustainalytics.ESGRatings.Computing.Entities.ESG.ReferenceUniverse;

namespace Sustainalytics.ESGRatings.BusinessLogic.Computing
{
    internal class ESGSet : ComputationSet
    {
        public Dictionary<Guid, int> Universes = new Dictionary<Guid, int>();
        public Dictionary<int, byte[]> EncodedUniverses = new Dictionary<int, byte[]>();

        public Dictionary<Guid, ESGCompanyEntity> Companies { get; set; }

        public Dictionary<Guid, CompanyScoresStruct> CompaniesScores { get; set; }


        private readonly object syncObject = new object();

        protected bool Equals(ESGSet other)
        {
            return Equals(Universes, other.Universes) && Equals(Matrices, other.Matrices)
                   && Equals(IndicatorsConfiguration, other.IndicatorsConfiguration)
                   && Equals(IndicatorScores, other.IndicatorScores) && Equals(SetType, other.SetType);
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj))
            {
                return false;
            }

            if (ReferenceEquals(this, obj))
            {
                return true;
            }

            if (obj.GetType() != GetType())
            {
                return false;
            }

            return Equals((ESGSet)obj);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                var hashCode = (Universes != null ? Universes.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ (Matrices != null ? Matrices.GetHashCode() : 0);
                hashCode = (hashCode * 397)
                           ^ (IndicatorsConfiguration != null ? IndicatorsConfiguration.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ (IndicatorScores != null ? IndicatorScores.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ SetType.GetHashCode();
                return hashCode;
            }
        }

        /// <exception cref="ComputeException">Exception when adding set</exception>
        /// <exception cref="ArgumentNullException"><paramref>
        ///         <name>key</name>
        ///     </paramref>
        ///     is null.</exception>
        /// <exception cref="ArgumentException">An element with the same key already exists in the <see cref="T:System.Collections.Generic.Dictionary`2" />.</exception>
        public ESGSet(IndicatorsConfiguration indicatorsConfiguration, ICollection<ESGCompanyEntity> companyEntities, Func<ESGCompanyEntity, string> sortKeySelector, IComparer<string> comparer, bool isActive)
            : base(companyEntities.Count, indicatorsConfiguration)
        {
            IsActive = isActive;

            ComparativeGroups.Add(0, IndexByHeads<Guid, string>.Create(companyEntities, x => x.Id, sortKeySelector, comparer));

            var attributes = companyEntities.Select(c => new KeyValuePair<Guid, byte>(c.Id, (byte)c.Attributes));

            Companies = companyEntities.ToDictionary(x => x.Id, x => x);

            try
            {
                SetAttributes(attributes);
            }
            catch (ESGException exception)
            {
                throw new ComputeException("Exception when adding set", exception);
            }
        }

        /// <exception cref="DuplicateEntityException">Same matrix in the set already was added.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="weights"/> is <see langword="null" />.</exception>
        /// <exception cref="OverflowException">The array is multidimensional and contains more than <see cref="F:System.Int32.MaxValue" /> elements.</exception>
        public override void AddMatrix<T>(Guid matrixId, IEnumerable<EntityIndicators<T, float>> weights)
        {
            if (weights == null)
            {
                throw new ArgumentNullException(nameof(weights));
            }

            if (Matrices.ContainsKey(matrixId))
            {
                throw new DuplicateEntityException(matrixId, typeof(WeightMatrixStruct));
            }

            var internalId = NextId();

            var weightMatrixInfo = new WeightMatrixStruct
            {
                IsNaN = new BitArray(CompaniesNo * IndicatorsNo, true),
                MatrixId = matrixId,
                MatrixInternalId = internalId
            };

            float[] weightData;
            byte[] profileHasEsgResearch;

            WeightsMatrixToArray(weights, CompaniesNo, IndicatorsNo, ref weightMatrixInfo, out weightData, out profileHasEsgResearch);

            Matrices.Add(matrixId, weightMatrixInfo);
        }

        public void AddCompaniesScores(ICollection<CompanyScores> companyScores)
        {
            float[] scoresData;

            scoresData = new float[CompaniesNo * 8]; // 0 initialized

            var aggregatedWeights = new float[CompaniesNo * 8];

            CompaniesScores = new Dictionary<Guid, CompanyScoresStruct>();


            foreach (var item in companyScores)
            {

                var companyId = item.CompanyId;
                int companyIndex = GetCompanyIndex(companyId);

                scoresData[GetColumnOrderIndex(companyIndex, 0, CompaniesNo)] = item.OverallRating.Score;
                scoresData[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.EIndex + 1, CompaniesNo)] = item.EnvironmentRating.Score;
                scoresData[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.SIndex + 1, CompaniesNo)] = item.SocialRating.Score;
                scoresData[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.GIndex + 1, CompaniesNo)] = item.GovernanceRating.Score;
                scoresData[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.DIndex + 4, CompaniesNo)] = item.DisclosureRating.Score;
                scoresData[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.PIndex + 4, CompaniesNo)] = item.PreparednessRating.Score;
                scoresData[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.QLPIndex + 4, CompaniesNo)] = item.QualitativePerformanceRating.Score;
                scoresData[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.QPIndex + 4, CompaniesNo)] = item.QuantitativePerformanceRating.Score;


                aggregatedWeights[GetColumnOrderIndex(companyIndex, 0, CompaniesNo)] = item.OverallRating.Weight;
                aggregatedWeights[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.EIndex + 1, CompaniesNo)] = item.EnvironmentRating.Weight;
                aggregatedWeights[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.SIndex + 1, CompaniesNo)] = item.SocialRating.Weight;
                aggregatedWeights[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.GIndex + 1, CompaniesNo)] = item.GovernanceRating.Weight;
                aggregatedWeights[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.DIndex + 4, CompaniesNo)] = item.DisclosureRating.Weight;
                aggregatedWeights[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.PIndex + 4, CompaniesNo)] = item.PreparednessRating.Weight;
                aggregatedWeights[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.QLPIndex + 4, CompaniesNo)] = item.QualitativePerformanceRating.Weight;
                aggregatedWeights[GetColumnOrderIndex(companyIndex, IndicatorsConfiguration.QPIndex + 4, CompaniesNo)] = item.QuantitativePerformanceRating.Weight;


                CompaniesScores.Add(item.CompanyId, new CompanyScoresStruct(item));
            }
        }



        /// <exception cref="DuplicateEntityException">Universe with same Id already added to set.</exception>
        public void AddUniverse(ReferenceUniverse universe, int comparativeGroupIndex = 0)
        {
            if (Universes.ContainsKey(universe.Id))
            {
                throw new DuplicateEntityException(universe.Id, typeof(ReferenceUniverse));
            }

            byte[] encodedUniverse = new byte[CompaniesNo]; // initialized la 0

            IIndex<Guid, string> comparativeGroup;

            if (ComparativeGroups.TryGetValue(comparativeGroupIndex, out comparativeGroup))
            {
                foreach (var company in universe.Companies)
                {
                    var companyId = company.CompanyId;
                    int index = comparativeGroup.GetIndex(companyId);
                    if (index < 0)
                    {
                        continue;
                    }

                    encodedUniverse[index] = 1;
                }

                var id = NextId();

                EncodedUniverses.Add(id, encodedUniverse);
                Universes.Add(universe.Id, id);
            }
        }

        public bool DeleteReferenceUniverse(Guid referenceUniverseId)
        {
            return true;
        }

        public IEnumerable<Guid> GetLoadedUniverses()
        {
            return Universes.Keys;
        }

        // TODO:  this is not tested and not correct

        /// <exception cref="ArgumentException">
        /// <paramref>
        ///         <name>comparisonType</name>
        ///     </paramref>
        ///     is not a <see cref="T:System.StringComparison"/> value. 
        /// </exception>
        /// <exception cref="Exception">
        /// A delegate callback throws an exception.
        /// </exception>
        /// <exception cref="NotSupportedException">
        /// <see cref="T:System.StringComparison"/> is not supported.
        /// </exception>
        /// <returns>
        /// The <see cref="IEnumerable"/>.
        /// </returns>
        public IEnumerable<Guid> GetCompaniesInGroup(List<string> comparativeGroupIds, Func<ESGCompanyEntity, string> selector)
        {
            return from kvp in Companies
                   let company = kvp.Value
                   where comparativeGroupIds != null && company != null
                   let found = comparativeGroupIds.Exists(
                       p => string.Compare(selector(company), p.ToString(), StringComparison.OrdinalIgnoreCase) == 0)
                   where found
                   select kvp.Key;
        }

        /// <exception cref="UnknownMatrixException">When matrix is not found in set.</exception>
        /// <exception cref="UnknownUniverseException">Condition.</exception>
        public ESGComputeResult Ratings(Guid matrixId, Guid refUnivId, ESGRatingsOptions.CompanyAttributesEnum filter, ComparativeGroupEnum groupBy, ESGRatingsOptions.IncludeFlags options, int comparativeGroupIndex = 0)
        {
            int internalUniverseId;
            WeightMatrixStruct matrixInfo;
            if (!Matrices.TryGetValue(matrixId, out matrixInfo))
            {
                throw new UnknownMatrixException(matrixId);
            }

            if (!Universes.TryGetValue(refUnivId, out internalUniverseId))
            {
                throw new UnknownUniverseException(refUnivId);
            }

            IIndex<Guid, string> comparativeGroup;
            if (!ComparativeGroups.TryGetValue(comparativeGroupIndex, out comparativeGroup))
            {
                throw new ESGEntityNotFound("Comparative groups is missing");
            }

            int[] groupByHeads = comparativeGroup.GroupByHeads;

            var esgResult = new ESGComputeResult
            {
                CompaniesHeadsGroups = (int[])groupByHeads.Clone(),
                CompaniesMap = comparativeGroup.GetReverseIndexCopy(),
                Configuration = IndicatorsConfiguration,
                Ratings = null,
                IndicatorsScores = IndicatorScores,
                MatrixInfo = matrixInfo,
                ComputeOptions = options,
                IsDefaultWeightMatrix = matrixId == DefaultMatrixId
            };

            return esgResult;
        }

        public byte[] CompanyAttributes;

        private void SetAttributes(IEnumerable<KeyValuePair<Guid, byte>> attributes)
        {
            CompanyAttributes = new byte[CompaniesNo]; // 0 initialized

            foreach (var companyAttribute in attributes)
            {
                var companyId = companyAttribute.Key;
                var companyIndex = GetCompanyIndex(companyId);

                CompanyAttributes[companyIndex] = companyAttribute.Value;
            }
        }

        private int GetCompanyIndex(Guid companyId, int comparativeGroupIndex = 0)
        {
            IIndex<Guid, string> comparativeGroup;
            if (!ComparativeGroups.TryGetValue(comparativeGroupIndex, out comparativeGroup))
            {
                throw new ESGEntityNotFound("Invalid comparative group");
            }

            int companyIndex = comparativeGroup.GetIndex(companyId);
            if (companyIndex < 0)
            {
                throw new UnknownCompanyException(companyId);
            }

            return companyIndex;
        }

        private void WeightsMatrixToArray<T>(IEnumerable<EntityIndicators<T, float>> companiesIndicatorsData, int companiesNo, int indicatorsNo, ref WeightMatrixStruct matrixInfo, out float[] weightData, out byte[] hasEsgResearch)
        {
            weightData = new float[companiesNo * indicatorsNo]; // 0 initialized
            hasEsgResearch = new byte[companiesNo];

            foreach (var item in companiesIndicatorsData)
            {
                var companyData = item as CompanyWeights;
                if (companyData?.IndicatorsData == null)
                {
                    continue;
                }

                var companyId = companyData.CompanyId;
                int companyIndex = GetCompanyIndex(companyId);
                hasEsgResearch[companyIndex] = companyData.ProfileHasEsgResearch ? (byte)1 : (byte)0;
                foreach (var dataPair in companyData.IndicatorsData)
                {
                    int indicatorIndex = IndicatorsConfiguration.GetIndex(dataPair.Code);
                    var index = GetColumnOrderIndex(companyIndex, indicatorIndex, companiesNo);
                    weightData[index] = dataPair.Value;
                    matrixInfo.IsNaN.Set(index, false);
                }
            }
        }
    }
}